Skip to content

Fix Hatch environments being invisible to stale-install checks and VEX (#335) - #700

Open
Mikola Lysenko (mikolalysenko) wants to merge 17 commits into
mainfrom
agent/fix-hatch-env-discovery
Open

Mikola Lysenko (mikolalysenko) wants to merge 17 commits into
mainfrom
agent/fix-hatch-env-discovery

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #335

Summary

On a Hatch project whose environment already exists (any developer checkout,
a warm CI cache), a hosted or vendored scan reported success with no warning.
The next hatch run kept the unpatched release, and vex attested
not_affected. Socket Patch now finds Hatch's own environments, warns with
the remedy that works (hatch env remove <env> / hatch env prune), and no
longer attests over an unpatched one.

Root cause

find_local_venv_site_packages (crawlers/python_crawler.rs) knows the
out-of-tree envs of Poetry, Pipenv, PDM and uv, but not Hatch's. Hatch never
uses ./.venv. It keeps a project's envs under
<data dir>/env/virtual/<project>/<sha256(root)[:8]>/<env>. Every consumer
of that discovery was therefore blind to the env hatch run uses: the hosted
redirect_pypi_stale_install probe, VEX's installed basis, and the agent-mode
crawl. The vendored Hatch flavour also never ran a stale-install probe. pip,
and uv before Hatch 1.16, keep a same-version release that is already
installed, and Hatch then records the env as synced, so the env stays
unpatched indefinitely.

Changes

  • crawlers/hatch_env.rs (new): models Hatch's placement rules, checked
    against Hatch's source for 1.0, 1.1, 1.2, 1.9 and 1.18:

    • HATCH_DATA_DIR, then dirs.data in HATCH_CONFIG or the platform
      config file, then the platform data dir (Linux XDG, macOS
      Application Support, Windows LOCALAPPDATA)
    • [dirs.env] virtual (absolute or project-relative; flat when inside the
      project or ~/.virtualenvs)
    • per-env path and HATCH_ENV_TYPE_VIRTUAL_PATH
    • the project id (urlsafe-b64 sha256 of the root, casefolded on
      Windows/macOS where newer Hatch does that)
    • <id>-unmanaged projects without a [project] table
    • the Hatch 1.0–1.2 <name>-<id>/<env> layout

    Config reads are FIFO-safe (read_regular_to_string). Paths spelled
    through a symlink (macOS /var → /private/var) still match.

  • python_crawler.rs: every existing Hatch env's site-packages is added
    next to whatever the generic probes found. That includes when an
    unrelated venv is activated, because hatch run never uses a foreign
    venv.

  • Hosted (scan/hosted/python.rs): a stale site inside a Hatch env gets
    the Hatch remedy naming the env.

  • Vendored (vendor/pypi.rs): the Pipenv-only probe is factored into
    stale_install_sites. The Hatch flavour judges Hatch's env prefixes
    directly and emits pypi_hatch_stale_install per stale env, on fresh and
    in-sync runs alike.

  • vex: vendored vendored_tree_out_of_sync also names
    hatch env remove <env> when the project has Hatch envs.

  • Docs: CLI_CONTRACT.md (new code row, stale-guard paragraph,
    out-of-sync note) and docs/testing/hatch.md.

Per-issue checklist

Test evidence

  • CI on 04610c1: all 491 check runs passed or were skipped, including the
    Hatch e2e matrix (1.0.0 / 1.2.1 / 1.9.7 / 1.14.2 / 1.18.1, Linux and
    macOS), the Linux, macOS and Windows tests, clippy and CodeQL. Bugbot is
    clean on this head.
  • Red → green:
    • hatch_out_of_tree_envs_are_project_envs fails without the
      crawler step.
    • hatch_vendor_warns_about_a_stale_hatch_env fails without the vendored
      probe (only vendor_prebuilt_downloaded, the issue's symptom).
    • hatch_existing_env_hosted (real Hatch 1.18.1) fails without the crawler
      step, with success and no warning. That is the issue's repro.
  • Real Hatch e2e, run locally: both modes pass on Hatch 1.0.0, 1.9.7 and
    1.18.1
    . The remedy is executed (hatch env remove default, then
    hatch run) and the env then holds the patched bytes, which vex
    attests.
  • Hash and layout checked against real Hatch 1.18.1:
    env/virtual/my-app/OuNYZq5s/my-app.
  • cargo fmt --check is not enforced in CI and main is not fmt-clean.
    Only this PR's lines were formatted; no unrelated reformatting.
  • Wrappers (npm/, pypi/, gem/) only dispatch to the binary, so no
    parallel change is needed.

Follow-ups (not in this PR)

  • The vendored_tree_out_of_sync detail is per purl, not per site, so it
    lists every Hatch env of the project rather than only the stale one.

🤖 Generated with Claude Code

https://claude.ai/code/session_018vDNsy9HnaC3kU2vrqc5j6


Note

Medium Risk
Changes which Python install trees are judged for hosted redirects, vendored warnings, and VEX attestations; incorrect Hatch path modeling could miss stale envs or warn on the wrong remedy, but behavior is heavily tested and scoped to Hatch projects.

Overview
Adds Hatch out-of-tree virtualenv discovery so stale-install checks, VEX, agent crawls, and vendored PyPI flows see the envs hatch run actually uses (not only ./.venv).

A new hatch_env crawler models Hatch’s data-dir layouts (config, explicit path, legacy 1.0–1.2, matrix names, etc.). find_local_venv_site_packages now unions those envs’ site-packages with existing probes; Pipenv’s probe uses non_hatch_local_venv_site_packages so it does not mis-attribute Hatch-only trees.

Hosted scan emits redirect_pypi_stale_install with hatch env remove / hatch env prune when a stale site lives in a Hatch env. Vendored Hatch adds pypi_hatch_stale_install via shared stale_install_sites logic. vex extends vendored_tree_out_of_sync with the same Hatch remedy. CLI contract and Hatch testing docs document the behavior; real-Hatch e2e covers pre-existing envs. Minor digest helper reuse in Gradle/JVM paths is unrelated to Hatch.

Reviewed by Cursor Bugbot for commit 97ce9b5. Configure here.


Generated by Claude Code

Hatch installs a project into envs under its data directory, never
./.venv, so stale-install checks, VEX and agent mode never looked at
the environment hatch run actually uses. Model Hatch's placement
rules (data dir, dirs.env.virtual, flat layouts, explicit env paths,
the project id hash) and add those envs to local venv discovery.

Hosted scans now warn about a stale Hatch env with the remedy that
works (hatch env remove / prune), and vendored Hatch gets the same
check as pypi_hatch_stale_install. A real-Hatch e2e covers both modes
from an existing env through vex and the remedy.

Fixes #335

Assisted-by: Claude Code:claude-opus-5-5
Hatch 1.0 to 1.2 keep envs at <name>-<id>/<env>, so discover that
layout too. Vendored vex already warns when the installed tree is
out of sync with the committed artifact; for a Hatch project the
advice to re-run the install does nothing, so name hatch env remove
instead.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
Explain where Hatch keeps environments, which warning each mode
gives for a stale one and the remedy, and how to run the real-Hatch
check.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] CI note on native (windows-latest, 1.1.38) at 2621cc4: 37/38 Bun cells passed. The one failure, two-versions hosted, hit WinError 10054 / "An existing connection was forcibly closed" against patches-api.socket.dev/patch/batch on all three of its in-script attempts. Several other cells in the same job hit the same transport error and then passed on retry. This PR changes only PyPI/Hatch environment discovery (crawlers/hatch_env.rs, python_crawler.rs, vendor/pypi.rs, the hosted Python stale probe and a vex warning detail), not Bun or the API client, so this isn't caused by the PR. Earlier agent PRs (#605, #617, #625) needed the same Bun Windows re-run. No code fix applies. I'll re-run the failed job once when the workflow run completes.


Generated by Claude Code

On macOS /var is a symlink to /private/var, so an activated env and
the discovered one can name the same directory differently, and the
stale-install check then missed it. Compare resolved paths as a
fallback, and leave dirs.env.virtual unresolved as Hatch does (only
an env's explicit path is resolved).

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 3, 2026 15:24
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/python_crawler.rs Outdated
An activated venv that is not one of Hatch's own is never used by
hatch run, yet it returned from discovery before the Hatch envs were
added, so a developer shell with any venv active hid the stale Hatch
env again. Add Hatch's envs in that case too, and have the vendored
probe judge Hatch's env prefixes directly.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@cursor

cursor Bot commented Oct 3, 2026

Copy link
Copy Markdown

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Hatch envs missed when VIRTUAL_ENV is set
    • Added Hatch project detection and modified VIRTUAL_ENV handling to ignore activated venvs for Hatch projects, ensuring Hatch environments are discovered even when VIRTUAL_ENV is set.

Create PR

Or push these changes by commenting:

@cursor push d5f09ab674
Preview (d5f09ab674)
diff --git a/crates/socket-patch-cli/src/commands/apply.rs b/crates/socket-patch-cli/src/commands/apply.rs
--- a/crates/socket-patch-cli/src/commands/apply.rs
+++ b/crates/socket-patch-cli/src/commands/apply.rs
@@ -2,9 +2,7 @@
 use socket_patch_core::api::blob_fetcher::get_missing_blobs;
 use socket_patch_core::api::client::{get_api_client_with_overrides, ApiClient};
 use socket_patch_core::crawlers::ruby_crawler::config_path_ignored_warning;
-use socket_patch_core::crawlers::{
-    detect_npm_pkg_manager, Ecosystem, NpmPkgManager, RubyCrawler,
-};
+use socket_patch_core::crawlers::{detect_npm_pkg_manager, Ecosystem, NpmPkgManager, RubyCrawler};
 use socket_patch_core::manifest::operations::read_manifest;
 use socket_patch_core::manifest::schema::{PatchFileInfo, PatchManifest, PatchRecord};
 use socket_patch_core::patch::apply::{

diff --git a/crates/socket-patch-cli/src/commands/list.rs b/crates/socket-patch-cli/src/commands/list.rs
--- a/crates/socket-patch-cli/src/commands/list.rs
+++ b/crates/socket-patch-cli/src/commands/list.rs
@@ -431,7 +431,10 @@
                 detail: detail.clone(),
             });
         } else if !args.common.silent {
-            eprintln!("Warning: {}", crate::commands::rollback::capitalize_first(detail));
+            eprintln!(
+                "Warning: {}",
+                crate::commands::rollback::capitalize_first(detail)
+            );
         }
     }
     let vendor_state = crate::commands::vendor_state_lenient(&loaded.vendor, args.common.silent);
@@ -773,12 +776,18 @@
         let listings = HostedListing::from_pins(
             &[
                 pin("pkg:npm/minimist@1.2.2", &record.uuid),
-                pin("pkg:npm/other@1.0.0", "33333333-3333-4333-8333-333333333333"),
+                pin(
+                    "pkg:npm/other@1.0.0",
+                    "33333333-3333-4333-8333-333333333333",
+                ),
             ],
             Some(&legacy),
         );
         assert_eq!(listings[0].record, record);
-        assert_eq!(listings[1].record.uuid, "33333333-3333-4333-8333-333333333333");
+        assert_eq!(
+            listings[1].record.uuid,
+            "33333333-3333-4333-8333-333333333333"
+        );
         assert!(listings[1].record.vulnerabilities.is_empty());
         assert_eq!(listings[1].lockfiles, vec!["yarn.lock".to_string()]);
     }

diff --git a/crates/socket-patch-cli/src/commands/mod.rs b/crates/socket-patch-cli/src/commands/mod.rs
--- a/crates/socket-patch-cli/src/commands/mod.rs
+++ b/crates/socket-patch-cli/src/commands/mod.rs
@@ -1,7 +1,7 @@
 pub mod apply;
 pub(crate) mod bun_preflight;
+pub(crate) mod composer_hints;
 pub(crate) mod context;
-pub(crate) mod composer_hints;
 pub(crate) mod fetch_stage;
 pub mod get;
 pub mod hosted_bundle;
@@ -9,11 +9,11 @@
 pub(crate) mod lock_cli;
 pub mod remove;
 pub mod repair;
-pub(crate) mod vendored_backend;
 pub mod rollback;
 pub mod scan;
 pub mod update;
 pub mod vendor;
+pub(crate) mod vendored_backend;
 pub mod vex;
 pub(crate) mod vex_consumed;
 pub(crate) mod vex_sources;
@@ -141,9 +141,11 @@
     common: &crate::args::GlobalArgs,
     root: &Path,
 ) -> socket_patch_core::patch::redirect::RedirectState {
-    hosted_state_from_pins(&socket_patch_core::patch::redirect::upstream::HostedPin::all(
-        &discover_wiring(common, root).await,
-    ))
+    hosted_state_from_pins(
+        &socket_patch_core::patch::redirect::upstream::HostedPin::all(
+            &discover_wiring(common, root).await,
+        ),
+    )
 }
 
 /// [`hosted_state_from_lockfiles`] over already-discovered pins. A purl
@@ -153,10 +155,8 @@
 ) -> socket_patch_core::patch::redirect::RedirectState {
     let mut state = socket_patch_core::patch::redirect::RedirectState::new();
     for pin in pins {
-        state
-            .records
-            .entry(pin.purl.clone())
-            .or_insert_with(|| socket_patch_core::manifest::schema::PatchRecord {
+        state.records.entry(pin.purl.clone()).or_insert_with(|| {
+            socket_patch_core::manifest::schema::PatchRecord {
                 uuid: pin.uuid.clone(),
                 exported_at: String::new(),
                 files: Default::default(),
@@ -164,7 +164,8 @@
                 description: String::new(),
                 license: String::new(),
                 tier: String::new(),
-            });
+            }
+        });
     }
     state
 }
@@ -191,4 +192,3 @@
         }
     }
 }
-

diff --git a/crates/socket-patch-cli/src/commands/remove.rs b/crates/socket-patch-cli/src/commands/remove.rs
--- a/crates/socket-patch-cli/src/commands/remove.rs
+++ b/crates/socket-patch-cli/src/commands/remove.rs
@@ -17,9 +17,9 @@
     pin_before_hash_blobs, rollback_patches_inner, run_hosted_leg, sweep_failure,
     sweep_unused_artifacts, HostedLegOutcome, InnerSelection,
 };
-use crate::commands::vendored_backend::{RevertedEntry, VendorRevertStep, VendoredBackend};
 use crate::args::{apply_env_toggles, GlobalArgs};
 use crate::commands::lock_cli::acquire_or_emit;
+use crate::commands::vendored_backend::{RevertedEntry, VendorRevertStep, VendoredBackend};
 use crate::json_envelope::{Command, Envelope, EnvelopeError, PatchAction, PatchEvent, Status};
 use crate::ui::plural;
 

diff --git a/crates/socket-patch-cli/src/commands/rollback.rs b/crates/socket-patch-cli/src/commands/rollback.rs
--- a/crates/socket-patch-cli/src/commands/rollback.rs
+++ b/crates/socket-patch-cli/src/commands/rollback.rs
@@ -10,13 +10,13 @@
 };
 use socket_patch_core::manifest::schema::{PatchFileInfo, PatchManifest, PatchRecord};
 use socket_patch_core::patch::apply::select_installed_variants;
+use socket_patch_core::patch::redirect::upstream::HostedPin;
 use socket_patch_core::patch::rollback::{
     cannot_rollback_error, rollback_package_patch, verify_file_rollback, RollbackResult,
     VerifyRollbackResult, VerifyRollbackStatus,
 };
 use socket_patch_core::telemetry::{track_patch_rollback_failed, track_patch_rolled_back};
 use socket_patch_core::utils::purl::{patch_matches, strip_purl_qualifiers};
-use socket_patch_core::patch::redirect::upstream::HostedPin;
 use socket_patch_core::vendor::{purl_keys_cover, RevertOpts, VendorState};
 use std::collections::{HashMap, HashSet};
 use std::path::{Path, PathBuf};
@@ -1026,7 +1026,8 @@
             .iter()
             .map(|(code, detail)| (code.to_string(), detail.clone())),
     );
-    out.edited_files.extend(outcome.reverted_files.iter().cloned());
+    out.edited_files
+        .extend(outcome.reverted_files.iter().cloned());
     let unwound: Vec<_> = vlt_targets
         .into_iter()
         .filter(|t| out.reverted.iter().any(|p| p == &t.purl))
@@ -1170,7 +1171,11 @@
             } else if !args.common.silent {
                 println!(
                     "{} the pre-v5 hosted ledger {}: no lockfile pins a hosted patch.",
-                    if args.common.dry_run { "Would remove" } else { "Removed" },
+                    if args.common.dry_run {
+                        "Would remove"
+                    } else {
+                        "Removed"
+                    },
                     socket_patch_core::patch::redirect::REDIRECT_STATE_REL
                 );
             }

diff --git a/crates/socket-patch-cli/src/commands/scan/discovery.rs b/crates/socket-patch-cli/src/commands/scan/discovery.rs
--- a/crates/socket-patch-cli/src/commands/scan/discovery.rs
+++ b/crates/socket-patch-cli/src/commands/scan/discovery.rs
@@ -168,29 +168,32 @@
     }
     // `(ledger key, base purl, entry)`; the artifact fallback has no
     // entries to probe, so it never reports unwired keys.
-    let candidates: Vec<(String, String, Option<&socket_patch_core::vendor::VendorEntry>)> =
-        match state {
-            Ok(state) => state
-                .entries
-                .iter()
-                .map(|(key, entry)| {
-                    (
-                        key.clone(),
-                        strip_purl_qualifiers(&entry.base_purl).to_string(),
-                        Some(entry),
-                    )
-                })
-                .collect(),
-            // Corrupt/unreadable ledger (a MISSING file is Ok(empty) above):
-            // recover the vendored set from the committed artifacts, or
-            // `scan --prune` (whose ledger exemption also degrades to empty)
-            // would delete still-vendored packages' manifest entries and blobs.
-            Err(_) => vendored_purls_from_artifacts(common)
-                .await
-                .into_iter()
-                .map(|base| (base.clone(), base, None))
-                .collect(),
-        };
+    let candidates: Vec<(
+        String,
+        String,
+        Option<&socket_patch_core::vendor::VendorEntry>,
+    )> = match state {
+        Ok(state) => state
+            .entries
+            .iter()
+            .map(|(key, entry)| {
+                (
+                    key.clone(),
+                    strip_purl_qualifiers(&entry.base_purl).to_string(),
+                    Some(entry),
+                )
+            })
+            .collect(),
+        // Corrupt/unreadable ledger (a MISSING file is Ok(empty) above):
+        // recover the vendored set from the committed artifacts, or
+        // `scan --prune` (whose ledger exemption also degrades to empty)
+        // would delete still-vendored packages' manifest entries and blobs.
+        Err(_) => vendored_purls_from_artifacts(common)
+            .await
+            .into_iter()
+            .map(|base| (base.clone(), base, None))
+            .collect(),
+    };
     // Composer by release identity: a ledger `@3.0.2.0` is the crawled
     // `@3.0.2`, not a second package to supplement.
     let key = |p: &str| composer_purl_identity(p).unwrap_or_else(|| normalize_purl(p).into_owned());
@@ -1038,7 +1041,9 @@
             ..GlobalArgs::default()
         };
         let state = socket_patch_core::vendor::load_state(root).await;
-        vendored_ledger_supplement(&args, crawled, &state).await.packages
+        vendored_ledger_supplement(&args, crawled, &state)
+            .await
+            .packages
     }
 
     /// A ledger entry vendored as `@3.0.2.0` is the crawled composer
@@ -1073,7 +1078,9 @@
             out.iter().map(|p| &p.purl).collect::<Vec<_>>()
         );
 
-        let out = vendored_ledger_supplement(&args, &[], &Ok(state)).await.packages;
+        let out = vendored_ledger_supplement(&args, &[], &Ok(state))
+            .await
+            .packages;
         assert_eq!(
             out.iter().map(|p| p.purl.as_str()).collect::<Vec<_>>(),
             vec!["pkg:composer/psr/log@3.0.2.0"]
@@ -1176,7 +1183,10 @@
             let state = npm_ledger_with_lock(tmp.path(), lock.as_deref()).await;
             let out = vendored_ledger_supplement(&args, &[], &state).await;
             assert_eq!(
-                out.packages.iter().map(|p| p.purl.as_str()).collect::<Vec<_>>(),
+                out.packages
+                    .iter()
+                    .map(|p| p.purl.as_str())
+                    .collect::<Vec<_>>(),
                 vec!["pkg:npm/left-pad@1.3.0"],
                 "lock={lock:?}"
             );

diff --git a/crates/socket-patch-cli/src/commands/scan/hosted.rs b/crates/socket-patch-cli/src/commands/scan/hosted.rs
--- a/crates/socket-patch-cli/src/commands/scan/hosted.rs
+++ b/crates/socket-patch-cli/src/commands/scan/hosted.rs
@@ -932,7 +932,8 @@
             socket_patch_core::utils::fs::read_regular_to_string_sync(path).ok()
         })
     };
-    let rewrite_options = || RewriteOptions {
+    let rewrite_options = || {
+        RewriteOptions {
         dry_run: common.dry_run,
         targets_pipenv_lock,
         pipenv_major,
@@ -944,6 +945,7 @@
         npm_allow_remote_config: !common.no_npm_allow_remote_config,
         npm_outer: &npm_outer,
         blocking: true,
+    }
     };
     // The rollout gate plans again without its deferred rows: keep what
     // the second pass needs.
@@ -2304,13 +2306,19 @@
 /// artifacts, then verify with `vex`. After a vendored→hosted takeover
 /// (`vendored_removed`) the commit also has to carry the deleted vendored
 /// ledger entries and artifacts.
-fn format_next_steps(files: &[String], edits: &[socket_patch_core::patch::redirect::FileEdit], vendored_removed: bool) -> Vec<String> {
+fn format_next_steps(
+    files: &[String],
+    edits: &[socket_patch_core::patch::redirect::FileEdit],
+    vendored_removed: bool,
+) -> Vec<String> {
     if files.is_empty() && !vendored_removed {
         return Vec::new();
     }
     let mut commit: Vec<String> = Vec::new();
     if vendored_removed {
-        commit.push(".socket/vendor/ (the removed vendored ledger entries and artifacts)".to_string());
+        commit.push(
+            ".socket/vendor/ (the removed vendored ledger entries and artifacts)".to_string(),
+        );
     }
     commit.extend(files.iter().cloned());
     let npm = files
@@ -4391,19 +4399,43 @@
         use super::npm_allow_remote_one_line;
         let hosts = ["patch.socket.dev"];
         let cases = [
-            (npm_allow_remote_configured_detail(&hosts, true, false), "Note: set"),
-            (npm_allow_remote_configured_detail(&hosts, false, false), "Note: set"),
-            (npm_allow_remote_configured_detail(&hosts, true, true), "Note: would set"),
-            (npm_allow_remote_already_detail(&hosts), "Note: .npmrc already"),
-            (npm_allow_remote_user_set_detail(&hosts, "none"), "Warning: npm >=12"),
-            (npm_allow_remote_env_set_detail(&hosts, "npm_config_allow_remote", "none"), "Warning: npm >=12"),
+            (
+                npm_allow_remote_configured_detail(&hosts, true, false),
+                "Note: set",
+            ),
+            (
+                npm_allow_remote_configured_detail(&hosts, false, false),
+                "Note: set",
+            ),
+            (
+                npm_allow_remote_configured_detail(&hosts, true, true),
+                "Note: would set",
+            ),
+            (
+                npm_allow_remote_already_detail(&hosts),
+                "Note: .npmrc already",
+            ),
+            (
+                npm_allow_remote_user_set_detail(&hosts, "none"),
+                "Warning: npm >=12",
+            ),
+            (
+                npm_allow_remote_env_set_detail(&hosts, "npm_config_allow_remote", "none"),
+                "Warning: npm >=12",
+            ),
             (npm_allow_remote_manual_detail(&hosts), "Warning: npm >=12"),
-            (npm_allow_remote_unreadable_detail(&hosts, "is a symlink"), "Warning: npm >=12"),
+            (
+                npm_allow_remote_unreadable_detail(&hosts, "is a symlink"),
+                "Warning: npm >=12",
+            ),
         ];
         for (detail, start) in cases {
             let line = npm_allow_remote_one_line(&detail);
             assert!(line.starts_with(start), "{line}");
-            assert!(!line.contains('\n') && line.ends_with("(details: --verbose)."), "{line}");
+            assert!(
+                !line.contains('\n') && line.ends_with("(details: --verbose)."),
+                "{line}"
+            );
         }
     }
 }

diff --git a/crates/socket-patch-cli/src/commands/scan/mod.rs b/crates/socket-patch-cli/src/commands/scan/mod.rs
--- a/crates/socket-patch-cli/src/commands/scan/mod.rs
+++ b/crates/socket-patch-cli/src/commands/scan/mod.rs
@@ -35,17 +35,17 @@
 
 use super::get::{download_and_apply_patches_with, DownloadParams, DownloadRun};
 
+use self::policy::{load_invocation_policy, InvocationPolicy, PolicyLoadError, ScanPolicy};
 pub use self::socket_yml_args::{SocketYmlArgs, MIN_SEVERITY_ENV};
-use self::policy::{load_invocation_policy, InvocationPolicy, PolicyLoadError, ScanPolicy};
 
 mod discovery;
 mod gc;
 pub(crate) mod hosted;
 pub(crate) mod policy;
-mod socket_yml_args;
 pub(crate) mod render;
 pub(crate) mod rollout;
 pub mod rollout_args;
+mod socket_yml_args;
 pub(crate) mod vendor_flow;
 
 use self::discovery::{
@@ -65,13 +65,13 @@
 pub(crate) use self::hosted::boxed_run_redirect_selected;
 use self::hosted::run_redirect;
 pub(crate) use self::hosted::{vlt_rollback_heal, vlt_takeover_heal};
-pub(crate) use self::vendor_flow::{
-    boxed_vendor_step, preview_vendor_json, print_dry_run_refusals, VendorStep,
-};
 use self::vendor_flow::{
     boxed_vendor_interactive_path, boxed_vendor_json_path, fold_vendored_skips_into_apply,
     partition_skipped_selected,
 };
+pub(crate) use self::vendor_flow::{
+    boxed_vendor_step, preview_vendor_json, print_dry_run_refusals, VendorStep,
+};
 
 /// Packages per batch request on the authenticated API when `--batch-size`
 /// is not given: the server's own per-request maximum
@@ -318,11 +318,7 @@
     /// `requests`), or a purl with or without its version
     /// (`pkg:npm/lodash`, `pkg:pypi/requests@2.31.0`). Repeat the flag or
     /// separate with commas
-    #[arg(
-        long = "package",
-        env = "SOCKET_SCAN_PACKAGES",
-        value_delimiter = ','
-    )]
+    #[arg(long = "package", env = "SOCKET_SCAN_PACKAGES", value_delimiter = ',')]
     pub packages: Vec<String>,
 
     /// On a successful scan, also generate an OpenVEX 0.2.0 document.
@@ -500,9 +496,10 @@
     telemetry.flush().await;
     let error_count = failures.len();
     if error_count > 0 && error_count == packages.len() {
-        let err = failures
-            .last()
-            .map_or_else(|| "all patch-detail queries failed".to_string(), |(_, e)| e.clone());
+        let err = failures.last().map_or_else(
+            || "all patch-detail queries failed".to_string(),
+            |(_, e)| e.clone(),
+        );
         let message = format!("all {error_count} patch-detail queries failed: {err}");
         if detail_error_line {
             eprintln!("{}", render::fetch_details_failed(&failures));
@@ -568,7 +565,11 @@
     packages: &[BatchPackagePatches],
     result: Option<&mut serde_json::Value>,
 ) -> Vec<rollout::Row> {
-    let failed: Vec<String> = discovered.failed.iter().map(|(purl, _)| purl.clone()).collect();
+    let failed: Vec<String> = discovered
+        .failed
+        .iter()
+        .map(|(purl, _)| purl.clone())
+        .collect();
     stage.incomplete = rollout::lookup_incomplete(&recorded.index, &failed, batch_failed);
     let rows = rollout::classify(&discovered.offers, &recorded.index, &stage.project);
     if let Some(result) = result {
@@ -1317,7 +1318,8 @@
         let joined = cwd.join(raw);
         if raw.contains(['*', '?', '[']) {
             let pattern = joined.to_string_lossy().into_owned();
-            let matches = glob::glob(&pattern).map_err(|e| format!("invalid path pattern `{raw}`: {e}"))?;
+            let matches =
+                glob::glob(&pattern).map_err(|e| format!("invalid path pattern `{raw}`: {e}"))?;
             let before = dirs.len();
             dirs.extend(
                 matches
@@ -1390,7 +1392,10 @@
     }
     // One budget per invocation (§5.2): the directories spend it in sorted
     // order, and a package admitted in one is admitted free in the next.
-    let configured = match args.rollout.resolve_from_env(invocation.policy.max_new_patches()) {
+    let configured = match args
+        .rollout
+        .resolve_from_env(invocation.policy.max_new_patches())
+    {
         Ok(max) => max,
         Err(message) => {
             eprintln!("Error: {message}");
@@ -1491,7 +1496,10 @@
     // error.
     let configured_cap = match args.rollout.carry.as_ref() {
         Some(carry) => carry.lock().configured,
-        None => match args.rollout.resolve_from_env(invocation.policy.max_new_patches()) {
+        None => match args
+            .rollout
+            .resolve_from_env(invocation.policy.max_new_patches())
+        {
             Ok(max) => max,
             Err(message) => {
                 eprintln!("Error: {message}");
@@ -1499,11 +1507,8 @@
             }
         },
     };
-    let mut stage = rollout::Stage::new(
-        configured_cap,
-        args.rollout.carry.clone(),
-        &args.common.cwd,
-    );
+    let mut stage =
+        rollout::Stage::new(configured_cap, args.rollout.carry.clone(), &args.common.cwd);
 
     // Strict airgap (CLI_CONTRACT.md `--offline`): scan's patch discovery
     // is remote data, so refuse before the crawl and before the API client
@@ -1704,8 +1709,11 @@
         .filter(|pkg| args.common.purl_ecosystem_selected(&pkg.purl))
         .collect();
 
-    let package_specs: Vec<&String> =
-        args.packages.iter().filter(|s| !s.trim().is_empty()).collect();
+    let package_specs: Vec<&String> = args
+        .packages
+        .iter()
+        .filter(|s| !s.trim().is_empty())
+        .collect();
     let filtered_crawled: Vec<_> = if package_specs.is_empty() {
         filtered_crawled
     } else {
@@ -1860,13 +1868,12 @@
                 // `redirectState` rides the empty-discovery envelope too
                 // (same rule as the ≥1-package path). `wiringLive` is empty
                 // by construction: this run covered zero packages.
-                let redirect_state = (!args.common.is_global()).then_some(
-                    crate::commands::hosted_state_from_pins(
+                let redirect_state =
+                    (!args.common.is_global()).then_some(crate::commands::hosted_state_from_pins(
                         &socket_patch_core::patch::redirect::upstream::HostedPin::all(
                             ctx.discovery().await,
                         ),
-                    ),
-                );
+                    ));
                 if let Some(state) = redirect_state_json(redirect_state.as_ref(), &[]) {
                     result["redirectState"] = state;
                 }
@@ -2222,7 +2229,8 @@
         // A report-only run selects nothing, but a severity floor or
         // `enabled: false` still hides candidates; report them like the
         // human arm does (the detail fetch runs only then).
-        if !apply && !vendor && policy.reports_selection() && !all_packages_with_patches.is_empty() {
+        if !apply && !vendor && policy.reports_selection() && !all_packages_with_patches.is_empty()
+        {
             if let Err((code, message)) = discover_selected(
                 &api_client,
                 &all_packages_with_patches,
@@ -2515,12 +2523,7 @@
                     &all_packages_with_patches,
                     None,
                 );
-                updates = offer_updates(
-                    &rows,
-                    &discovered,
-                    &recorded,
-                    &all_packages_with_patches,
-                );
+                updates = offer_updates(&rows, &discovered, &recorded, &all_packages_with_patches);
                 rows
             }
             // `discover_selected` already printed the failure to stderr.
@@ -2982,14 +2985,20 @@
             dirs.iter()
                 .map(|(d, explicit)| {
                     (
-                        d.strip_prefix(tmp.path()).unwrap().to_string_lossy().replace('\\', "/"),
+                        d.strip_prefix(tmp.path())
+                            .unwrap()
+                            .to_string_lossy()
+                            .replace('\\', "/"),
                         *explicit,
                     )
                 })
                 .collect()
         };
-        let got = project_dirs(tmp.path(), &["apps/*".into(), "libs/core".into(), "apps/web".into()])
-            .unwrap();
+        let got = project_dirs(
+            tmp.path(),
+            &["apps/*".into(), "libs/core".into(), "apps/web".into()],
+        )
+        .unwrap();
         // Named literally = explicit (also when a glob matches it too).
         assert_eq!(
             rel(got),

diff --git a/crates/socket-patch-cli/src/commands/scan/policy.rs b/crates/socket-patch-cli/src/commands/scan/policy.rs
--- a/crates/socket-patch-cli/src/commands/scan/policy.rs
+++ b/crates/socket-patch-cli/src/commands/scan/policy.rs
@@ -11,9 +11,9 @@
 use socket_patch_core::api::types::PatchSearchResult;
 use socket_patch_core::manifest::schema::PatchManifest;
 use socket_patch_core::policy::{
-    canon, find_repo_root_with_warnings, policy_block, FilteredEntry, RetainedEntry, patch_severity_order, repo_relative_checked, sanitize, severity_name,
-    DiskPolicyFs, FilterReason, Offers, PolicyError, PolicySource, PolicyWarning, Root, SelectionPolicy,
-    PATCHES_DISABLED,
+    canon, find_repo_root_with_warnings, patch_severity_order, policy_block, repo_relative_checked,
+    sanitize, severity_name, DiskPolicyFs, FilterReason, FilteredEntry, Offers, PolicyError,
+    PolicySource, PolicyWarning, RetainedEntry, Root, SelectionPolicy, PATCHES_DISABLED,
 };
 use socket_patch_core::utils::purl::normalize_purl;
 
@@ -42,12 +42,18 @@
 /// Load the policy for `args` (4.5): `--global` scans have no repo and read
 /// no file; everything else reads the repo root's socket.yml.
 pub(crate) fn load_invocation_policy(args: &ScanArgs) -> Result<InvocationPolicy, PolicyLoadError> {
-    let overrides = args.socket_yml.overrides().map_err(PolicyLoadError::Usage)?;
+    let overrides = args
+        .socket_yml
+        .overrides()
+        .map_err(PolicyLoadError::Usage)?;
     let cwd = std::fs::canonicalize(&args.common.cwd).unwrap_or_else(|_| args.common.cwd.clone());
     if args.common.is_global() {
-        let policy = SelectionPolicy::load(&socket_patch_core::policy::MemoryPolicyFs::default(), &overrides)
-            .map_err(PolicyLoadError::Policy)?
-            .0;
+        let policy = SelectionPolicy::load(
+            &socket_patch_core::policy::MemoryPolicyFs::default(),
+            &overrides,
+        )
+        .map_err(PolicyLoadError::Policy)?
+        .0;
         return Ok(InvocationPolicy {
             policy,
             repo_root: cwd,
@@ -56,8 +62,8 @@
         });
     }
     let (repo_root, mut warnings) = find_repo_root_with_warnings(&cwd);
-    let (policy, load_warnings) =
-        SelectionPolicy::load(&DiskPolicyFs::new(&repo_root), &overrides).map_err(PolicyLoadError::Policy)?;
+    let (policy, load_warnings) = SelectionPolicy::load(&DiskPolicyFs::new(&repo_root), &overrides)
+        .map_err(PolicyLoadError::Policy)?;
     warnings.extend(load_warnings);
     Ok(InvocationPolicy {
         policy,
@@ -138,7 +144,12 @@
 
 impl ScanPolicy {
     /// The policy for the project rooted at `root_dir`.
-    pub(crate) fn for_root(invocation: &InvocationPolicy, root_dir: &Path, explicit: bool, global: bool) -> Self {
+    pub(crate) fn for_root(
+        invocation: &InvocationPolicy,
+        root_dir: &Path,
+        explicit: bool,
+        global: bool,
+    ) -> Self {
         let root_dir = std::fs::canonicalize(root_dir).unwrap_or_else(|_| root_dir.to_path_buf());
         let project = repo_relative_checked(&invocation.repo_root, &root_dir).unwrap_or_default();
         let root_verdict = if global {
@@ -171,7 +182,9 @@
                 severity: None,
             });
         }
-        let announce_warnings = !invocation.warned.swap(true, std::sync::atomic::Ordering::Relaxed);
+        let announce_warnings = !invocation
+            .warned
+            .swap(true, std::sync::atomic::Ordering::Relaxed);
         Self {
             policy: invocation.policy.clone(),
             warnings,
@@ -224,7 +237,10 @@
     /// exclude stays in the query (so `upgradeAvailable` can be reported)
     /// but joins the retained set, which never reaches a writer.
     pub(crate) fn admit_crawled(&self, purl: &str) -> bool {
-        let verdict = self.root_verdict.clone().and_then(|()| self.policy.admits_purl(purl));
+        let verdict = self
+            .root_verdict
+            .clone()
+            .and_then(|()| self.policy.admits_purl(purl));
         let reason = match verdict {
             Ok(()) => return true,
             Err(reason) => reason,
@@ -334,7 +350,8 @@
             // (not when a lower-ranked admitted patch simply wins).
             let top_withheld = self.policy.admits_severity(patch_severity_order(&group[0]));
             if let Err(reason) = top_withheld {
-                let upgrade_withheld = chosen.is_some() && chosen == recorded_at && recorded_at != Some(0);
+                let upgrade_withheld =
+                    chosen.is_some() && chosen == recorded_at && recorded_at != Some(0);
                 if chosen.is_none() || upgrade_withheld {
                     report.filtered.push(FilteredEntry {
                         purl: Some(canon(&purl)),
@@ -522,17 +539,20 @@
         let verdict = if !policy.enabled() {
             Err(FilterReason::Disabled)
         } else {
-            root_verdict.clone().and_then(|()| policy.admits_purl(purl)).and_then(|()| {
-                // The floor only hides a package when none of its patches pass.
-                match group
-                    .iter()
-                    .map(|p| policy.admits_severity(patch_severity_order(p)))
-                    .find(Result::is_ok)
-                {
-                    Some(ok) => ok,
-                    None => policy.admits_severity(patch_severity_order(group[0])),
-                }
-            })
+            root_verdict
+                .clone()
+                .and_then(|()| policy.admits_purl(purl))
+                .and_then(|()| {
+                    // The floor only hides a package when none of its patches pass.
+                    match group
+                        .iter()
+                        .map(|p| policy.admits_severity(patch_severity_order(p)))
+                        .find(Result::is_ok)
+                    {
+                        Some(ok) => ok,
+                        None => policy.admits_severity(patch_severity_order(group[0])),
+                    }
+                })
         };
         if let Err(reason) = verdict {
             out.push((

diff --git a/crates/socket-patch-cli/src/commands/scan/render.rs b/crates/socket-patch-cli/src/commands/scan/render.rs
--- a/crates/socket-patch-cli/src/commands/scan/render.rs
+++ b/crates/socket-patch-cli/src/commands/scan/render.rs
@@ -746,7 +746,10 @@
 
     #[test]
     fn report_only_hint_names_agent_mode() {
-        assert_eq!(report_only_hint()[0], "To apply these patches in place, run:");
+        assert_eq!(
+            report_only_hint()[0],
+            "To apply these patches in place, run:"
+        );
         assert!(report_only_hint()[1].contains("--mode agent"));
     }
 

diff --git a/crates/socket-patch-cli/src/commands/scan/rollout.rs b/crates/socket-patch-cli/src/commands/scan/rollout.rs
--- a/crates/socket-patch-cli/src/commands/scan/rollout.rs
+++ b/crates/socket-patch-cli/src/commands/scan/rollout.rs
@@ -4,8 +4,10 @@
 
 use std::collections::{BTreeMap, BTreeSet, HashSet};
 
-use socket_patch_core::rollout::{canonical_base_purl, severity_label, MaxNew, MaxNewSource, Recorded, RolloutPlan};
 pub(crate) use socket_patch_core::rollout::stage::*;
+use socket_patch_core::rollout::{
+    canonical_base_purl, severity_label, MaxNew, MaxNewSource, Recorded, RolloutPlan,
+};
 
 use super::discovery::UpdateInfo;
 
@@ -208,11 +210,11 @@
 mod tests {
     use super::*;
     use socket_patch_core::api::types::PatchSearchResult;
+    use socket_patch_core::api::types::VulnerabilityResponse;
     use socket_patch_core::manifest::schema::PatchManifest;
-    use std::path::Path;
-    use socket_patch_core::api::types::VulnerabilityResponse;
     use socket_patch_core::manifest::schema::PatchRecord;
     use std::collections::HashMap;
+    use std::path::Path;
 
     fn offer(purl: &str, uuid: &str, published: &str, severities: &[&str]) -> PatchSearchResult {
         PatchSearchResult {
@@ -357,13 +359,21 @@
         let stored = manifest(&[("pkg:composer/psr/log@3.0.2.0", "old")]);
         let recorded = RecordedIndex::new(Some(&stored), &[]);
         let offers = offers_from_results(
-            &[offer("pkg:composer/psr/log@v3.0.2", "new", "2026-02-01T00:00:00Z", &["high"])],
+            &[offer(
+                "pkg:composer/psr/log@v3.0.2",
+                "new",
+                "2026-02-01T00:00:00Z",
+                &["high"],
+            )],
             false,
         );
         let rows = classify(&offers, &recorded, "");
         let plan = socket_patch_core::rollout::plan_rollout(
             rows.into_iter().map(|row| row.candidate).collect(),
-            &MaxNew { value: Some(0), source: MaxNewSource::Flag },
+            &MaxNew {
+                value: Some(0),
+                source: MaxNewSource::Flag,
+            },
             false,
             &BTreeSet::new(),
         );

diff --git a/crates/socket-patch-cli/src/commands/scan/rollout_args.rs b/crates/socket-patch-cli/src/commands/scan/rollout_args.rs
--- a/crates/socket-patch-cli/src/commands/scan/rollout_args.rs
+++ b/crates/socket-patch-cli/src/commands/scan/rollout_args.rs
@@ -1,7 +1,6 @@
 //! `scan --max-new-patches` (see the rollout guide,
 //! `docs/configuration.md#gradual-rollout`).
 
-
 use clap::Args;
 pub(crate) use socket_patch_core::rollout::stage::RolloutCarry;
 use socket_patch_core::rollout::{resolve_max_new, MaxNew};
@@ -77,7 +76,6 @@
     }
 }
 
-
 #[cfg(test)]
 mod tests {
     use super::*;

diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs
--- a/crates/socket-patch-cli/src/commands/vendor.rs
+++ b/crates/socket-patch-cli/src/commands/vendor.rs
@@ -257,10 +257,7 @@
... diff truncated: showing 800 of 6941 lines

You can send follow-ups to the cloud agent here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review (burn-down agent).

  • Head: 04610c192b9d7c292ef02ad4848932adec784877, 0 commits behind main
  • CI: 485/485 check runs passed (6 skipped, 0 failed or pending)
  • Bugbot: clean on 04610c1. Its one earlier finding (Hatch envs missed when VIRTUAL_ENV is set) was fixed in 04610c1, and the thread is resolved.
  • Reviewers should look at: the Hatch placement model in crawlers/hatch_env.rs (data-dir and config precedence, project-id hashing, legacy 1.0–1.2 layout).

Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 5, 2026
main restructured the activated-venv step so Poetry projects go through
poetry_project_site_packages; keep that and add Hatch's out-of-tree envs
to the non-Poetry activated-venv result, which is what this branch did
before the restructure (#335). In CLI_CONTRACT.md keep main's reworded
pypi_pipenv_stale_install row and this branch's pypi_hatch_stale_install row.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

1 similar comment
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/python_crawler.rs Outdated
Comment thread crates/socket-patch-core/src/crawlers/hatch_env.rs
main added a pipenv_lock parameter to the hosted Python stale-install
probe; the Hatch remedy test from this branch still called it with
the old arity, so the CLI test build broke after the merge.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] e2e-build (ubuntu-latest) on 4d5c944 failed to compile socket-patch-cli lib tests: main added a pipenv_lock: Option<&str> parameter to stale_install_warnings, and this PR's hatch_env_gets_the_stale_install_warning_with_hatch_remedy still called it with the old arity (python.rs:268, :280). Fixed in 9c84e0e by passing None. Verified locally with cargo test --workspace --all-features --no-run (all targets compile), the hosted stale-install tests (6/6) and the core Hatch tests (43/43). I'm pushing only this fix and leaving the rest of the burn-down agent's in-progress work alone.


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/python_crawler.rs Outdated
A Hatch project's pyproject alone reads as a uv project, so a set
UV_PROJECT_ENVIRONMENT returned from discovery before Hatch's envs
were added, hiding the env hatch run uses again. Add them on that
path too.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
Instead of appending Hatch's envs at each early return, wrap the
whole local discovery so a recorded PDM/uv env, an activated venv,
Pipenv's or Poetry's resolution all keep the project's Hatch envs
visible to stale-install checks, agent mode and VEX.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/python_crawler.rs
Hatch envs are now part of local discovery, so the vendored Pipenv
probe judged them too and told users to fix a Hatch env with pipenv
sync, which never clears it. Skip Hatch's envs there; the project's
own venv still gets the Pipenv warning.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

main is red: since the store-copy change (#605) the npm resolver
already returns alias and nested-store copies, so two vex_consumed
tests that assumed an alias-free set fail on main and on this
branch. Same change as #851; it no-ops once main carries it.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] coverage on e33afcb failed in socket-patch-cli --lib: vex_consumed::tests::hosted_expands_alias_only_copies and hosted_reuses_expanded_npm_copies_and_merges_alias_variants. This isn't caused by this PR. Both tests fail identically on main (4646693), because since #605 the npm resolver already returns the alias and nested-store copies these tests assumed were absent. The fix is open as #851. I've ported the same change here so this PR doesn't wait on it; it becomes a no-op once #851 lands. With it, all 841 CLI lib tests pass locally.


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/vendor/pypi.rs Outdated
Filtering every Hatch-claimed site out of the vendored Pipenv probe
also dropped a venv the two share (a Hatch env with path = .venv),
which pipenv sync does reinstall, so neither probe warned. Judge
exactly the venvs local discovery resolves before Hatch's envs are
added instead.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 5, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: labeled Ready for review.

  • Head: 67df5163f25944e353b11ff4023a0f239890b3ec
  • CI: 491/491 check runs green (success/skipped) on this head; mergeable clean
  • Bugbot: reviewed 67df516, no new issues; earlier threads resolved
  • Reviewer focus: Hatch environment discovery feeding stale-install warnings and VEX

Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 5, 2026
Resolved conflict in crates/socket-patch-core/src/crawlers/mod.rs: main
added the gradle_cache and jvm_cache modules while this branch added
hatch_env at the same spot; kept all three in alphabetical order.

Co-Authored-By: Claude <noreply@anthropic.com>
Fixes clippy::items_after_test_module under --all-targets.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/hatch_env.rs
When Hatch's env directory is the shared ~/.virtualenvs, only the env
names the project configures were looked up there. Matrix variants
such as test.py3.11 and the hatch-test.py3.X envs that `hatch test`
creates were never found. A stale install in one of them therefore got
no stale-install warning, and VEX could attest over it.

The lookup now builds the matrix names the way Hatch does (Python
variable first as py<version>, matrix-name-format, <env>. prefix
except for default). It also takes hatch-test.* unless the project
configures its own hatch-test env. Checked against Hatch 1.18.1's
`hatch env show`.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] test (macos-latest), test (windows-latest) and test-release failed on a2f0e4d in utils::digest::tests::production_digests_go_through_the_helpers. This failure isn't from this PR. The guard flags crawlers/gradle_cache.rs, patch/jvm_jar.rs and patch/sidecars/maven.rs, all inherited from main via the merge of #646 and #865, and this PR doesn't touch them. I ported the existing fix from agent/ci-gradle-digest-helpers (659ac2c, "Route Gradle digests through utils::digest") as a cherry-pick. It will no-op once main carries it. The test failed locally before the port and passes after it.

The earlier coverage and composer 2.9.8 / php 8.4 / ubuntu-latest failures on a2f0e4d were runner shutdowns mid-compile, before any test ran. The new head re-runs them.


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/hatch_env.rs
A project's [tool.hatch.envs.hatch-test] table is layered over Hatch's
built-in hatch-test config, so the default Python matrix still applies
unless the project sets its own matrix. The ~/.virtualenvs lookup
skipped hatch-test.* whenever the table existed at all, which hid
those envs from the stale-install checks and VEX. It now skips them
only when the project defines its own hatch-test matrix. Checked
against Hatch 1.18.1's `hatch env show`.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 97ce9b5. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Every red check on 97ce9b5 is a job GitHub never started. The jobs are cancelled with "The job was not acquired by Runner of type hosted even after multiple attempts", across CI, Bun, Go, vlt, PDM, Pipenv, npm and pnpm, and the queue is backed up for other branches too. vlt lock-diff fails only because the Windows and macOS native builds it compares never ran. No test failed.

I re-ran the failed jobs once in CI, Bun, Go, vlt, PDM and Pipenv. npm and pnpm already had their one re-run at 20:16 and were starved again, so they will re-run on the next push. Nothing in the diff needs to change for these.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hosted Hatch rewrite leaves an existing Hatch environment unpatched with no stale-install warning, and vex still attests not_affected

3 participants